<?php
/**
 * Nordic Repository
 * 
 * LICENSE
 * 
 * The new BSD license is applied on this source-file. For further
 * information please visit http://license.nordic-dev.de/newbsd.txt
 * or send an email to andre.moelle@gmail.com.
 */

/**
 * Necessary includes.
 */
Zend_Loader::loadClass('Zend_Db_Table_Abstract');
Zend_Loader::loadClass('Nordic_Db_Table');
Zend_Loader::loadClass('Nordic_Db_Model_Exception');

/**
 * This class bundles the tables for a component. 
 * 
 * Although you can call most methods of the table-objects using this
 * class, you should prefer encapsulating queries and other recurring
 * tasks in this class rather than in the table-classes, because it
 * is often needed to connect these tables.
 * 
 * @category   Nordic
 * @package    Nordic_Db
 * @copyright  2007 Nordic Development
 * @license    http://license.nordic-dev.de/newbsd.txt (New-BSD license)
 * @author     Andre Moelle <andre.moelle@gmail.com>
 * @version    $Id: Model.php 3 2007-07-08 09:21:42Z andre.moelle $
 */
abstract class Nordic_Db_Model
{
	/**
	 * Instance of a database-object.
	 * 
	 * @var Zend_Db_Adapter
	 */
	protected $_db;
	
	/**
	 * List of methods that overlap in the tables.
	 * 
	 * @var array
	 */
	protected $_methods = array();
	
	/**
	 * Array of table-objects (Zend_Db_Table_Abstract).
	 * 
	 * The key represents an alias for tables while the value is the
	 * corresponding instance.
	 * 
	 * <code>
	 * $_tables = array(
	 * 	'blog' => new App_Blog_Table(),
	 * 	'tags' => new App_Blog_Table_Tag()
	 * );
	 * </code>
	 * 
	 * @var array
	 */
	protected $_tables = array();
	
	/**
	 * Calls the abstract init-method.
	 * 
	 * Since the model-object should always be instantiated correctly,
	 * the constructor is final and cannot be overwritten by new
	 * constructors. The Template-Method pattern is applied here.
	 * 
	 * @param void
	 * @return void
	 */
	final public function __construct ()
	{
		$this->_db = Zend_Db_Table_Abstract::getDefaultAdapter();
		$this->_init();
		$this->_findOverlappingMethods();
	}
	
	/**
	 * Proxies the calls to the table-objects.
	 * 
	 * To determine which table-object is called, the first parameter
	 * of each method-class has to determine the name of the table.
	 * 
	 * <code>
	 * $name = 'fetchNew';
	 * $args = array('blog');
	 * // would call $this->_tables['blog']->fetchNew()
	 * $name = 'update';
	 * $args = array('tags', array('foo' => 'bar'), 'id = 42');
	 * // would call $this->_tables['tags']->update(array('foo' => 'bar'), 'id = 42')
	 * </code>
	 * 
	 * If the table-object does not exist, a "Nordic_Db_Model_Exception" will
	 * be thrown.
	 * 
	 * There are some cases where it is not needed to provide the table-aliases as
	 * first parameter. Mainly this is when the method is defined in this class,
	 * which means that the __call-method is not called, or when the method-name
	 * is unique.
	 * 
	 * <code>
	 * // only one table-object (alias is "foobar") defines the method "doFoobar"
	 * $name = 'doFoobar';
	 * $args = array();
	 * // would call $this->_tables['foobar']->doFoobar();
	 * </code>
	 * 
	 * This feature was implemented to provide better encapsulation and backwards
	 * compatibility.
	 * 
	 * @param string $name name of the method
	 * @param array $args arguments used for calling
	 * @return mixed
	 * @throws Nordic_Db_Model_Exception
	 */
	public function __call ($name, $args)
	{
		if(!($alias = $this->_overlaps($name)))
		{
			$alias = array_shift($args);
		}
		
		$obj = $this->get($alias);
		
		if(!$args)
		{
			return $obj->$name();
		}
		
		return call_user_func_array(array($obj, $name), $args);
	}
	
	/**
	 * Attaches a table to the model.
	 * 
	 * @param string $alias alias for the table
	 * @param Zend_Db_Table_Abstract $table table-object
	 * @return void
	 */
	protected function _attach ($alias, Zend_Db_Table_Abstract $table)
	{
		$this->_tables[$alias] = $table;
		
		if($table instanceof Nordic_Db_Table)
		{
			$table->setModel($this);
		}
	}
	
	/**
	 * Finds overlapping methods of the tables.
	 * 
	 * This is necessary to redirect some calls directly to the
	 * corresponding object without using the first parameter as
	 * alias.
	 * 
	 * @param void
	 * @return integer
	 */
	protected function _findOverlappingMethods ()
	{
		$result = 0;
		$methods = $this->_methods = array();
		
		foreach($this->_tables as $alias => $value)
		{
			foreach(get_class_methods($value) as $method)
			{
				if(!isset($methods[$method]))
				{
					$methods[$method] = array();
				}
				
				$methods[$method][] = $alias;
			}
		}
		
		foreach($methods as $method => $tables)
		{
			if(count($tables) > 1)
			{
				continue;
			}
			
			$this->_methods[$method] = $tables[0];
			$result++;
		}
		
		return $result;
	}
	
	/**
	 * Initializes the model.
	 * 
	 * It should create at least the table-classes.
	 * 
	 * @param void
	 * @return void
	 */
	abstract protected function _init ();
	
	/**
	 * Checks whether a method overlaps on some tables.
	 * 
	 * @param string $method name of the method
	 * @return string
	 */
	protected function _overlaps ($method)
	{
		if(!isset($this->_methods[$method]))
		{
			return '';
		}
		
		return $this->_methods[$method];
	}
	
	/**
	 * Returns an instance of the given table.
	 * 
	 * If the specified table does not exist, an exception will be
	 * thrown.
	 * 
	 * @param string $name name of the table
	 * @return Zend_Db_Table_Abstract
	 * @throws Nordic_Db_Model_Exception
	 */
	public function get ($name = '')
	{
		if(!isset($this->_tables[$name]))
		{
			if(count($this->_tables) != 1)
			{
				throw new Nordic_Db_Model_Exception('Table with the alias "' . $name . '" does not exist');
			}
			
			return reset($this->_tables);
		}
		
		return $this->_tables[$name];
	}
	
	/**
	 * Returns the name of the table for the given alias.
	 * 
	 * @param string $name alias of the table
	 * @return string
	 * @throws Nordic_Db_Model_Exception
	 */
	public function getName ($name = '')
	{
		$info = $this->get($name)->info();
		
		return $info[Zend_Db_Table_Abstract::NAME];
	}
	
	/**
	 * Returns the a special primary-key with the table-name prepended.
	 * 
	 * @param string $name alias of the table
	 * @param integer $position position of the primary key (1 for first, 2 for second, ...)
	 * @return string
	 * @throws Nordic_Db_Model_Exception
	 */
	public function getPrimary ($name = '', $position = 1)
	{
		$info = $this->get($name)->info();
		
		return $this->getName($name) . '.' . $info[Zend_Db_Table_Abstract::PRIMARY][$position];
	}
}
?>